using System;
using System.Collections.Generic;
using System.Linq;

namespace Bookvar.ObjectModel.Actions
{
    /// <summary>
    /// This class represents an action that is composed by other actions. It should be seen as one atomic action. 
    /// So when this action is executed it will execute its internal actions. 
    /// Consequently, when it is undone it will undo its actions.
    /// </summary>
    public class CompositeAction : ActionBase
    {
        private readonly IList<IAction> actions = new List<IAction>();
        private IList<IAction> executedActions;

        /// <summary>
        /// Gets a value indicating whether this instance can redo. 
        /// If all internal actions can redo this is <c>true</c>; otherwise <c>false</c>.
        /// </summary>
        /// <value><c>true</c> If all internal actions can redo this is <c>true</c>; otherwise <c>false</c>.</value>
        public override bool CanRedo
        {
            get { return actions.All( a => a.CanRedo ); }
        }

        /// <summary>
        /// Gets a value indicating whether this instance can undo. 
        /// If all internal actions can undo this is <c>true</c>; otherwise <c>false</c>.
        /// </summary>
        /// <value><c>true</c> If all internal actions can undo this is <c>true</c>; otherwise <c>false</c>.</value>
        public override bool CanUndo
        {
            get { return actions.All( a => a.CanUndo ); }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="CompositeAction"/> class.
        /// </summary>
        public CompositeAction()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="CompositeAction"/> class.
        /// </summary>
        /// <exception cref="ArgumentNullException">if <param name="action"/> is <c>null</c></exception>
        public CompositeAction( IAction action )
        {
            if ( action == null ) throw new ArgumentNullException( "action" );

            Add( action );
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="CompositeAction"/> class.
        /// </summary>
        /// <param name="firstAction">The first action to add.</param>
        /// <param name="secondAction">The second action to add.</param>
        /// <exception cref="ArgumentNullException">if <paramref name="firstAction"/> or 
        /// <paramref name="secondAction"/> is <c>null</c></exception>
        public CompositeAction( IAction firstAction, IAction secondAction )
        {
            if ( firstAction == null ) throw new ArgumentNullException( "firstAction" );
            if ( secondAction == null ) throw new ArgumentNullException( "secondAction" );

            Add( firstAction );
            Add( secondAction );
        }

        /// <summary>
        /// Adds the specified action to this composite action. The last added action will be executed last.
        /// The action will be executed when the composite action is executed.
        /// </summary>
        /// <param name="action">The action to add.</param>
        /// <exception cref="ArgumentNullException">if <paramref name="action"/> is null.</exception>
        public void Add( IAction action )
        {
            if ( action == null ) throw new ArgumentNullException( "action" );
            actions.Add( action );
        }

        /// <summary>
        /// Adds the specified action to this composite action as an executed action. This means that 
        /// the action will not be executed the first time the composite action is executed.
        /// After the first execute the action will be undone and executed/redone normally.
        /// </summary>
        /// <remarks>
        /// Generally you should avoid adding executed actions. This should be used only when you have to create
        /// a composite action from actions and some of the actions depend on the result of the executions of some 
        /// of the other actions.
        /// // TODO: Think of a better way to handle that case.
        /// </remarks>
        /// <param name="action">The action to add.</param>
        /// <exception cref="ArgumentNullException">if the <paramref name="action"/> is <c>null</c>.</exception>
        public void AddExecuted( IAction action)
        {
            if ( action == null ) throw new ArgumentNullException( "action" );

            actions.Add( action );

            if ( executedActions == null )
            {
                executedActions = new List<IAction>();
            }

            executedActions.Add( action );
        }

        /// <summary>
        /// Executes all the actions in the order they were added.
        /// </summary>
        /// <remarks>
        /// If an action is added as already executed it will not be executed the first time. 
        /// See <see cref="AddExecuted"/> for more information.
        /// </remarks>
        public override void Execute()
        {
            // TODO: Add transaction-like logic to catch exceptions and undoes the before the error executed actions.
            foreach ( IAction action in actions )
            {
                if ( !IsActionExecuted( action ) )
                {
                    action.Execute();
                }
            }

            ClearExecutedActions();
        }

        private bool IsActionExecuted( IAction action )
        {
            if ( executedActions == null )
            {
                return false;
            }

            return executedActions.Contains( action );
        }

        private void ClearExecutedActions()
        {
            executedActions = null;
        }

        /// <summary>
        /// Undoes all the actions in the reverse order in which they were executed/added.
        /// </summary>
        public override void Undo()
        {
            foreach ( IAction action in actions.Reverse() )
            {
                action.Undo();
            }
        }

        protected override void Dispose( bool disposing )
        {
            if ( disposing )
            {
                foreach ( var action in actions )
                {
                    action.Dispose();
                }
            }

            base.Dispose( disposing );
        }
    }
}
